//
//  DXMObject.m
//  DerbyStatter
//
//  Created by glenn andreas on 1/6/14.
//  Copyright (c) 2014 glenn andreas. All rights reserved.
//

#import "DXMLObject.h"
#import "DXMLTimeStamp.h"
@interface DXMLObject()
@property (readwrite, strong) NSMutableArray *children;
@property (readwrite, strong) NSMutableDictionary *attributes;
//@property (readwrite, strong) NSMutableString *text;
@end
@interface DXMLObjectUnknown : DXMLObject
@property (readwrite, strong) NSString *elementName;
@end
@interface DXMLObjectText : DXMLObject
@end

@class DXMLObjectFinderToken;
@interface DXMLObjectFinder : NSEnumerator
@property (strong) DXMLObject *root;
@property (strong) NSString * path;
@property (strong) NSArray *pathComponents;
@property (strong) DXMLObjectFinderToken *rootFinder;
@end

@implementation DXMLObject
@synthesize text = _text;
- (id) initWithElementName: (NSString *) elementName attributes: (NSDictionary *) attributes
{
    // we remap the XML tag to the class name
    // Yes, this isn't perfect, since both Foo and foo are mapped to the same class
    // and FooBar, foo-bar, Foo-Bar, etc.. are also all mapped to the same
    NSString *originalElementName = elementName;
    NSArray *parts = [elementName componentsSeparatedByString:@"-"];
    NSMutableArray *canonParts = [NSMutableArray array];
    for (NSString *part in parts) {
        [canonParts addObject: [part capitalizedString]];
    }
    elementName = [canonParts componentsJoinedByString:@""];
    Class cls = NSClassFromString([NSString stringWithFormat: @"DXML%@Object",elementName]);
    if (!cls) {
        cls = [DXMLObjectUnknown class];
    }
    if ([self class] != cls) {
//        [self release];
        self = [cls alloc];
    }
    self = [super init];
    if (self) {
        if (cls == [DXMLObjectUnknown class]) {
            // ugh - we only need to save the elementName for foriegn elements
            ((DXMLObjectUnknown *)self).elementName = originalElementName;
        }
        _attributes = [attributes mutableCopy];
    }
    return self;
}
- (NSString *) description
{
    NSMutableString *header = [NSMutableString stringWithFormat:@"<%@: %p",NSStringFromClass([self class]),self];
    [self.attributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        [header appendFormat:@" %@=\"%@\"",key,obj];
    }];
    [header appendString:@">"];
    return header;
}
- (void) setAttribute: (NSString *) attribute forKey: (NSString *) key
{
    if (!self.attributes)
        self.attributes = [NSMutableDictionary dictionary];
    if (!attribute)
        [(NSMutableDictionary *)self.attributes removeObjectForKey:key];
    else
        [(NSMutableDictionary *)self.attributes setObject:attribute forKey:key];
}
- (NSString *) attributeForKey: (NSString *) key;
{
    return [self.attributes objectForKey:key];
}

- (void) setIntAttribute: (NSInteger) attribute forKey: (NSString *) key
{
    if (!self.attributes)
        self.attributes = [NSMutableDictionary dictionary];
    [(NSMutableDictionary *)self.attributes setObject:[NSString stringWithFormat:@"%zd",attribute] forKey:key];
}
- (NSInteger) intAttributeForKey: (NSString *) key
{
    return [[self.attributes objectForKey:key] integerValue];
}
- (NSInteger) intAttributeForKey: (NSString *) key defaultValue: (NSInteger) defaultValue;
{
    id v = [self.attributes objectForKey:key];
    if (!v) return defaultValue;
    return [v integerValue];
}
- (void) setBoolAttribute: (BOOL) attribute forKey: (NSString *) key
{
    if (attribute) {
        if (!self.attributes)
            self.attributes = [NSMutableDictionary dictionary];
        [(NSMutableDictionary *)self.attributes setObject:@"true" forKey:key];
    } else {
        [(NSMutableDictionary *)self.attributes removeObjectForKey:key];
    }
}
- (BOOL) boolAttributeForKey: (NSString *) key
{
    return [[self.attributes objectForKey:key] boolValue];
}
- (BOOL) boolAttributeForKey:(NSString *)key defaultValue:(BOOL)defaultValue
{
    id v = [self.attributes objectForKey:key];
    if (!v) return defaultValue;
    return [v boolValue];
}
- (void) addChild: (DXMLObject *) child
{
    if (!self.children) {
        self.children = [NSMutableArray array];
    }
    if ([self.children containsObject:child])
        return;
    child.parent = self;
    [(NSMutableArray *)self.children addObject: child];
}
- (void) removeChild: (DXMLObject *) child;
{
    [(NSMutableArray *)self.children removeObjectIdenticalTo:child];
}
- (void) appendText: (NSString *) text
{
    if (self.children.count == 0) {
        if (!self.text)
            self.text = [text mutableCopy];
        else
            [(NSMutableString *)self.text appendString:text];
    } else {
        // make a special "blank" child with just text
        DXMLObjectText *textChild = [[DXMLObjectText alloc] init];
        textChild.text = [text mutableCopy];
        [self addChild: textChild];
    }
}
+ (NSString *) elementName
{
    NSString *name = NSStringFromClass(self);
    // remove DXML header and Object trailer
    name = [name substringWithRange:NSMakeRange(4, name.length - 10)];
    return [name lowercaseString]; // doesn't handle multi-part names yet
}
- (NSString *) elementName
{
    return [[self class] elementName];
}
// and a simple convert back to XML
- (void) appendXML: (NSMutableArray *) array indent: (NSString *) indent
{
    NSString *tag = [self elementName];
    NSMutableString *header = [NSMutableString stringWithFormat:@"%@<%@",indent,tag];
    if (self.attributes.count) {
        [self.attributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
            [header appendFormat:@" %@=\"%@\"",key,[obj stringByReplacingOccurrencesOfString:@"&" withString:@"&amp;"]];
        }];
    }
    // trim the whitespace on the text and escape
    NSString *text = [self.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
    text = [text stringByReplacingOccurrencesOfString:@"&" withString:@"&amp;"];
    text = [text stringByReplacingOccurrencesOfString:@"<" withString:@"&lt;"];
    text = [text stringByReplacingOccurrencesOfString:@">" withString:@"&gt;"];
    if (self.children.count == 0 && text.length == 0) {
        [header appendString:@"/>"];
        [array addObject:header];
    } else {
        [header appendString:@">"];
        BOOL multiLineText = [text rangeOfString:@"\n"].location != NSNotFound;
        if (text.length && !multiLineText && self.children.count == 0) { // <a>text</a>
            [header appendString:@">"];
            [header appendString:text];
            [header appendString:[NSString stringWithFormat:@"</%@>",tag]];
            [array addObject:header];
            return;
        }
        [array addObject:header];
        NSString *indent2 = [indent stringByAppendingString:@"\t"];
        if (text) {
            [array addObject:text];
        }
        for (DXMLObject *child in self.children) {
            [child appendXML:array indent:indent2];
        }
        [array addObject:[NSString stringWithFormat:@"%@</%@>",indent,tag]];
    }
}

- (NSString *) xmlString
{
    NSMutableArray *lines = [NSMutableArray array];
    [self appendXML:lines indent:@""];
    return [lines componentsJoinedByString:@"\n"];
}
- (DXMLObject *) find: (NSString *) path
{
    DXMLObjectFinder *finder = [DXMLObjectFinder new];
    finder.path = path;
    finder.root = self;
    return [finder nextObject];
}
- (NSArray *) findAll: (NSString *) path
{
    DXMLObjectFinder *finder = [DXMLObjectFinder new];
    finder.path = path;
    finder.root = self;
    return [finder allObjects];
}
- (id) firstChildWithElementName: (NSString *) name
{
    for (DXMLObject *child in self.children) {
        if ([child.elementName isEqualToString:name])
            return child;
    }
    return nil;
}
- (void) setFirstChild: (DXMLObject *)child withElementName: (NSString *) name
{
    DXMLObject *oldFirst = [self firstChildWithElementName:name];
    if (oldFirst) {
        NSInteger index = [self.children indexOfObjectIdenticalTo:oldFirst];
        if (!child)
            [(NSMutableArray *)self.children removeObjectAtIndex:index];
        else
            [(NSMutableArray *)self.children replaceObjectAtIndex:index withObject:child];
        child.parent = self;
    } else {
        [self addChild:child];
    }
}
- (NSArray *) allChildrenWithElementName: (NSString *) name
{
    NSMutableArray *array = [NSMutableArray array];
    for (DXMLObject *child in self.children) {
        if ([child.elementName isEqualToString:name]) {
            [array addObject:child];
        }
    }
    return array;
}
- (void) setChildren: (NSArray *)children withElementName: (NSString *) name
{
    NSArray *old = [self allChildrenWithElementName:name];
    [(NSMutableArray *)self.children removeObjectsInArray:old];
    for (DXMLObject *obj in children) {
        [self addChild:obj];
    }
}
- (id) enclosingParentOfClass: (Class) cls
{
    id retval = [self parent];
    while (retval && ![retval isKindOfClass:cls]) {
        retval = [retval parent];
    }
    return retval;
}
//- (NSString *) description
//{
//    return [NSString stringWithFormat:@"<%@: %p (%@) %@>", [self class], self, self.name, self.uuid.stringValue];
//}
- (float) nameIsSimilar: (NSString *) name
{
    if ([self.name caseInsensitiveCompare:name] == NSOrderedSame)
        return 1.0;
    // remove the spaces, etc...
    NSString *stripSelf = [self filterName:name withOnlyCharactersInSet:[NSCharacterSet letterCharacterSet]];
    NSString *other = [self filterName:name withOnlyCharactersInSet:[NSCharacterSet letterCharacterSet]];
    if ([stripSelf caseInsensitiveCompare:other] == NSOrderedSame)
        return 0.95; // close
    // and for now
    return 0.0;
}
- (NSString *) filterName: (NSString *) name withOnlyCharactersInSet: (NSCharacterSet *) set
{
    NSInteger len = name.length;
    unichar buffer[len+1];
    unichar *p = buffer;
    for (NSInteger i=0;i<len;i++) {
        unichar c = [name characterAtIndex:i];
        if ([set characterIsMember:c]) {
            *p = c;
            p++;
        }
    }
    return [NSString stringWithCharacters:buffer length:p - buffer];
    
}
DXMLSTRINGATTR(name, Name)

@synthesize timestamp = _timestamp;
- (DXMLTimestamp *) timestamp
{
    // use cache if possible
    if (_timestamp)
        return _timestamp;
    NSString *timestamp = [self attributeForKey:@"timestamp"];
    if (!timestamp)
        return nil;
    _timestamp = [[DXMLTimestamp alloc] initWithString: timestamp forObject:self];
    return _timestamp;
}
- (void) invalidateChildrenTimestamps
{
    // since time stamps are often relative to our parent, and
    // we cache these, we need to invalidate them when the
    // parent changes
    _timestamp = nil;
    for (DXMLObject *child in self.children) {
        [child invalidateChildrenTimestamps];
    }
}
- (void) setTimestamp:(DXMLTimestamp *)timestamp
{
    [self invalidateChildrenTimestamps];
    _timestamp = timestamp;
    [self setAttribute:[timestamp timestampValue] forKey:@"timestamp"];
}
- (DXMLTimestamp *) closestTimestamp
{
    DXMLTimestamp *retval = self.timestamp;
    if (!retval) {
        retval = self.parent.closestTimestamp;
    }
    return retval;
}
@end


@implementation DXMLObjectUnknown
@synthesize elementName = _elementName;
@end

@implementation DXMLObjectText
- (void) appendXML: (NSMutableArray *) array indent: (NSString *) indent
{
    if (self.text.length) {
        [array addObject:self.text];
    }
}
- (NSString *) xmlString
{
    return [self.text copy];
}
@end


#pragma mark XPath subset support
/* The way this works is to break each "clause" in the xpath into a separate iterator like object which we can reuse as
 we walk through the results.
*/
@interface DXMLObjectFinderToken : NSEnumerator
@property (strong)DXMLObject * root;
@property (assign) DXMLObject *lastObject;
@property (strong) DXMLObjectFinderToken *child;
- (DXMLObject *) iterate;
@end
@interface DXMLObjectFinderAllChildren : DXMLObjectFinderToken // "*"

@end
@interface DXMLObjectFinderAllChildrenWithTag : DXMLObjectFinderAllChildren // "tag"
@property (strong) NSString *tag;
@end

@interface DXMLObjectFinderCurrentNode : DXMLObjectFinderToken
@end

@interface DXMLObjectFinderAllSubElement : DXMLObjectFinderToken
@end
@interface DXMLObjectFinderParent : DXMLObjectFinderToken
@end
@interface DXMLObjectFinderAllElementsWithAttribute : DXMLObjectFinderAllChildren
@property (strong) NSString *attribute;
@end
@interface DXMLObjectFinderAllElementsWithAttributeValue : DXMLObjectFinderAllElementsWithAttribute
@property (strong) NSString *value;
@end
@interface DXMLObjectFinderAllElementsWithChildrenWithTag : DXMLObjectFinderAllChildrenWithTag
@end
@interface DXMLObjectFinderAllElementsAtPosition : DXMLObjectFinderToken
@property (assign) NSInteger pos; // if <0, then relative to the end (so last() == -1, last()-1 == -2)
@end


@implementation DXMLObjectFinderToken
- (DXMLObject *) iterate
{
    // this does the "hard" work
    return nil;
}
- (id) nextObject
{
    if (self.child) {
        id retval = self.child.nextObject; // get child from deepest iteration
        if (retval) {
            return retval;
        }
        // advance our state, and try the child again
        while (1) {
            DXMLObject *childRoot = self.iterate;
            if (childRoot) {
                self.child.root = childRoot;
                self.child.lastObject = nil;
                retval = self.child.nextObject;
                if (retval) break;
            } else {
                // we are out of states, depend on our parent to look for something
                break;
            }
        }
    } else {
        return self.iterate; // we are the bottom, this is the best we can do
    }
    return nil;
}
@end
@implementation DXMLObjectFinderAllChildren {
    NSInteger index;
}
- (DXMLObject *) iterate
{
    // we need to iterate with an index just in case the same element is twice in the element
    if (self.lastObject == nil) {
        index = 0;
    } else {
        index++;
    }
    if (index >= self.root.children.count)
        return nil;
    self.lastObject = self.root.children[index];
    return self.lastObject;
}
@end
@implementation DXMLObjectFinderAllChildrenWithTag
- (DXMLObject *) iterate
{
    while (1) {
        DXMLObject *retval = [super iterate];
        if (retval) {
            if ([retval.elementName isEqualToString:self.tag])
                return retval;
        } else {
            break;
        }
    }
    return nil;
}
@end
@implementation DXMLObjectFinderCurrentNode
- (DXMLObject *) iterate
{
    if (self.lastObject == nil) {
        self.lastObject = self.root;
        return self.root;
    }
    return nil;
}
@end
@implementation DXMLObjectFinderAllSubElement {
    NSMutableArray *subIterators;
}
- (DXMLObject *) iterate
{
    if (self.lastObject == nil) {
        if (self.root.children.count)
            subIterators = [NSMutableArray arrayWithObject: [self.root.children objectEnumerator]];
        else
            subIterators = [NSMutableArray array]; // empty, because no children
        self.lastObject = self.root;
        return self.root; // start here
    }
    while (subIterators.count) {
        NSEnumerator *iter = [subIterators lastObject];
        DXMLObject *next = [iter nextObject];
        if (next) {
            if (next.children.count) {
                // there are children here, we will get to them next
                [subIterators addObject:next.children.objectEnumerator];
            }
            return next;
        } else {
            // done with this level
            [subIterators removeLastObject];
        }
    }
    return nil;
}
@end
@implementation DXMLObjectFinderParent
- (DXMLObject *) iterate
{
    if (self.lastObject == nil) {
        self.lastObject = self.root.parent;
        return self.root.parent;
    }
    return nil;
}
@end
@implementation DXMLObjectFinderAllElementsWithAttribute
- (DXMLObject *) iterate
{
    while (1) {
        DXMLObject *retval = [super iterate];
        if (retval) {
            if ([retval.attributes objectForKey:self.attribute])
                return retval;
        } else {
            break;
        }
    }
    return nil;
}
@end
@implementation DXMLObjectFinderAllElementsWithAttributeValue
- (DXMLObject *) iterate
{
    while (1) {
        DXMLObject *retval = [super iterate];
        if (retval) {
            if ([[retval.attributes objectForKey:self.attribute] isEqualToString:self.value])
                return retval;
        } else {
            break;
        }
    }
    return nil;
}
@end
@implementation DXMLObjectFinderAllElementsAtPosition
- (DXMLObject *) iterate
{
    if (self.lastObject == nil) {
        NSArray *children = self.root.children;
        if (self.pos < 0) {
            // so -1 => childrenCount - 1
            NSInteger relPos = children.count - (-self.pos);
            if (relPos >= 0)
                self.lastObject = children[relPos];
        } else {
            if (children.count > self.pos) {
                self.lastObject = children[self.pos];
            }
        }
        return self.lastObject;
    }
    return nil;
}
@end
@implementation DXMLObjectFinderAllElementsWithChildrenWithTag
- (DXMLObject *) iterate
{
    if (self.lastObject == nil) {
        for (DXMLObject *obj in self.root.children) {
            if ([[obj elementName] isEqualToString:self.tag]) {
                self.lastObject = obj;
                return obj;
            }
        }
    }
    return nil;
}
@end

@implementation DXMLObjectFinder
- (id) nextObject
{
    static NSCharacterSet *attributeCharSet = nil;
    static NSCharacterSet *valueCharSet = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSMutableCharacterSet *cs = [[NSCharacterSet alphanumericCharacterSet] mutableCopy];
        [cs addCharactersInString:@"-_"];
        attributeCharSet = cs;
        cs = [[NSCharacterSet alphanumericCharacterSet] mutableCopy];
        [cs formUnionWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]];
        [cs formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
        [cs removeCharactersInString:@"'"];
        valueCharSet = cs;
    });
//    DXMLObject *retval = nil;
    if (!self.rootFinder) {
        if ([self.path hasSuffix:@"/"])
            self.path = [self.path stringByAppendingString:@"*"]; // implicit all (is this actually required?)
        NSScanner *scanner = [NSScanner scannerWithString:self.path];
        NSMutableArray *components = [NSMutableArray array];
        while (![scanner isAtEnd]) {
            NSString *token = nil;
            DXMLObjectFinderToken *finder = nil;
            if ([scanner scanString:@"*" intoString:&token]) {
                finder = [DXMLObjectFinderAllChildren new];
            } else if ([scanner scanString:@".." intoString:&token]) {
                finder = [DXMLObjectFinderParent new];
            } else if ([scanner scanString:@"." intoString:&token]) {
                finder = [DXMLObjectFinderCurrentNode new];
            } else if ([scanner scanString:@"//" intoString:&token]) {
                finder = [DXMLObjectFinderAllSubElement new];
            } else if ([scanner scanString:@"[" intoString:&token]) {
                if ([scanner scanString:@"@" intoString:nil]) {
                    NSString *attribute;
                    if ([scanner scanCharactersFromSet:attributeCharSet intoString:&attribute]) {
                        NSString *value;
                        if ([scanner scanString:@"=" intoString:nil] && [scanner scanString:@"'" intoString:nil] && [scanner scanUpToString:@"'" intoString:&value] && [scanner scanString:@"'" intoString:nil]) {
                            finder = [DXMLObjectFinderAllElementsWithAttributeValue new];
                            ((DXMLObjectFinderAllElementsWithAttributeValue *)finder).attribute = attribute;
                            ((DXMLObjectFinderAllElementsWithAttributeValue *)finder).value = value;
                        } else {
                            finder = [DXMLObjectFinderAllElementsWithAttribute new];
                            ((DXMLObjectFinderAllElementsWithAttribute *)finder).attribute = attribute;
                        }
                    }
                } else if ([scanner scanCharactersFromSet:[NSCharacterSet decimalDigitCharacterSet] intoString:&token]) {
                    NSInteger pos = [token integerValue] - 1;
                    if (pos >= 0) {
                        finder = [DXMLObjectFinderAllElementsAtPosition new];
                        ((DXMLObjectFinderAllElementsAtPosition *)finder).pos = pos;
                    }
                } else if ([scanner scanString:@"last()" intoString:&token]) {
                    if ([scanner scanString:@"-" intoString:nil]) {
                        if ([scanner scanCharactersFromSet:[NSCharacterSet decimalDigitCharacterSet] intoString:&token]) {
                            NSInteger pos = [token integerValue];
                            if (pos >= 0) {
                                finder = [DXMLObjectFinderAllElementsAtPosition new];
                                ((DXMLObjectFinderAllElementsAtPosition *)finder).pos = -(pos+1);
                            }
                        }
                    } else {
                        finder = [DXMLObjectFinderAllElementsAtPosition new];
                        ((DXMLObjectFinderAllElementsAtPosition *)finder).pos = -1; // -1 is last
                    }
                } else if ([scanner scanCharactersFromSet:attributeCharSet intoString:&token]) {
                    finder = [DXMLObjectFinderAllChildrenWithTag new];
                    ((DXMLObjectFinderAllChildrenWithTag *) finder).tag = token;
                }
                [scanner scanString:@"]" intoString:nil];
            } else if ([scanner scanCharactersFromSet:attributeCharSet intoString:&token]) {
                finder = [DXMLObjectFinderAllChildrenWithTag new];
                ((DXMLObjectFinderAllChildrenWithTag *) finder).tag = token;
            }
            if (finder)
                [components addObject:finder];
            else
                break;
            NSUInteger pos = [scanner scanLocation];
            if ([scanner scanString:@"//" intoString:nil]) {
                [scanner setScanLocation:pos];
            } else if ([scanner scanString:@"/" intoString:nil]) {
                // scan the separating "/" without grabbing a "//"
            }
        }
        DXMLObjectFinderToken *parent = nil;
        for (DXMLObjectFinderToken *token in components) {
            if (parent == nil)
                self.rootFinder = token;
            else
                parent.child = token;
            parent = token;
        }
        self.rootFinder.root = self.root;
        self.rootFinder.lastObject = nil;
    }
    return self.rootFinder.nextObject;
}


@end


